NodeJS-Express
- The “front end” denotes the interface that a web user interacts with - what they see (and hear) when using the web.
- The “back end”, meanwhile, denotes all that goes on “behind the scenes” on web servers to make the user experience possible.
I. NodeJS
Node is an asynchronous event driven JavaScript runtime.
- When JavaScript was first created, it was designed to run in the browser.
- Node lets it run anywhere (your machine, a server)
Node adds capabilities JS didn't have in the browser:
- Read/write local files
- Create HTTP connections
- Listen to network requests
- Browser JS:
// Change a button's text when clicked
document.querySelector("button").addEventListener("click", () => {
document.querySelector("h1").textContent = "Hello!";
});
- Node.js — things only possible outside the browser
// Create a server that listens for requests
const http = require("http");
http.createServer((req, res) => {
res.end("Hello from the server!");
}).listen(3000);
Async / Event-driven
Node doesn't wait for slow tasks (file I/O, DB queries) — it starts them and moves on
- When a task finishes, it fires an event and runs the next function (callback)
- Same idea as addEventListener in frontend JS, but for server-side events
- Result: can handle many things at once without blocking
- Synchronous: Read file → wait → done → query DB → wait → done
- Node (async): Read file + query DB at the same time → handle whichever finishes first
III. Core Node Modules
1. Running Scripts
- Node lets you run any JS file directly in the terminal (no browser needed)
node myfile.js→ executes the file and prints output to your terminal- This is the most basic thing Node adds: run JS like a program, not a webpage
2. HTTP Module — creating a local server
- When you write frontend code, you're used to just opening an HTML file in the browser
- In backend dev, you need an actual server running that "listens" for requests and sends back responses
http.createServer()sets up that server.listen(3000)tells it which port to watch — port 3000 is just a common convention for local dev
- While the script is running, anyone who visits http://localhost:3000 triggers your callback
- "localhost" means your own machine — this server is only accessible to you locally, not the internet
- In production, you'd deploy this to a real server with a real domain
- req = the incoming request (who's asking, what page they want)
- res = your response back to them
const http = require("http");
http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/html" }); // 200 = OK status code
res.end("Hello World!"); // what gets sent back to the browser
}).listen(3000);`
3. File System (fs) Module — reading and writing files
- Browser JS cannot touch your filesystem for security reasons — Node can
fs.readFile()reads a file and gives you the contents inside a callback (async!)fs.writeFile()creates or overwrites a file on disk- The "utf8" argument tells Node to give you the file as readable text, not raw bytes
fs.readFile("notes.txt", "utf8", (err, data) => {
if (err) throw err;
console.log(data); // prints file contents to terminal
});
fs.writeFile("notes.txt", "new content", (err) => {
if (err) throw err;
console.log("File saved!");
});
4. URL Module — parsing a URL into readable parts
- When a request comes into your server, you need to know what page they're asking for
- The URL module breaks a full URL string into useful pieces you can work with
const url = new URL("https://example.com/about?name=cam");
url.pathname // "/about" → which page they want
url.searchParams.get("name") // "cam" → query string values
5. Events / EventEmitter — creating your own custom events
- Node has a built-in EventEmitter class you can use to create, fire, and listen for your own events
.on("eventName", callback)→ listen for an event (same idea as addEventListener in the browser).emit("eventName")→ manually trigger/fire that event
Example:In real apps you won't use EventEmitter directly that often, but understanding it helps you understand how Node itself works under the hood
const EventEmitter = require("events");
const emitter = new EventEmitter();
emitter.on("greet", () => console.log("Hello!")); // set up the listener
emitter.emit("greet"); // fire it → prints "Hello!"
Note:
require()is the older Node way to import modules — same idea asimportin modern JS. You'll see both in the wild.
II. Express
Express is a minimal, unopinionated backend framework built on top of Node.js
- Writing a server with raw Node's
httpmodule gets verbose fast - Express wraps all of that and gives you a cleaner, simpler way to handle requests
- "Unopinionated" = it doesn't force you to structure your app any particular way
1. Setting Up
npm init -y # creates package.json
npm install express
// app.js
const express = require("express");
const app = express();
app.get("/", (req, res) => res.send("Hello, world!"));
const PORT = process.env.PORT || 3000; // use env variable, fallback to 3000
app.listen(PORT, (error) => {
if (error) throw error;
console.log(`Listening on port ${PORT}!`);
});
express()initializes your server and stores it inappapp.get()defines a route — more on this belowapp.listen()opens the door on that port, same idea as Node's.listen(3000)process.env.PORTis how you read environment variables — useful so you don't hardcode the port
2. How a Request Travels Through Express
When a browser visits http://localhost:3000/:
- Browser sends a GET request to the
/path - Express receives it and stores it in a request object (
req) - Express passes it through a chain of middleware functions
- The first route that matches the HTTP verb + path handles it
- That route sends back a response (
res) and the cycle ends
Visiting any URL in a browser is always just sending a GET request to that path.
https://theodinproject.com/paths= GET request to/pathsattheodinproject.com
3. Routes — matching requests to handlers
- A route matches an incoming request by HTTP verb (GET, POST, PUT, DELETE) + path
- Order matters — Express matches the first route that fits, top to bottom
app.get("/messages", (req, res) => res.send("GET messages"));
app.post("/messages", (req, res) => res.send("POST message"));
a. Route Parameters
- Dynamic segments in the path, prefixed with
: - Express populates
req.paramsautomatically
// GET /odin/messages → req.params = { username: "odin" }
app.get("/:username/messages", (req, res) => {
console.log(req.params.username); // "odin"
});
b. Query Parameters
- Optional key-value pairs after
?in the URL — not part of the path itself - Express populates
req.queryautomatically
// GET /messages?sort=date&direction=asc
// req.query = { sort: "date", direction: "asc" }
Real example you've seen:
youtube.com/watch?v=abc123&t=60—/watchis the path,vandtare query params
c. Routers — organizing routes into files
- In a real app with many routes, you split them into separate files by resource
- Each file exports a Router, mounted in
app.jsunder a base path
// routes/authorRouter.js
const { Router } = require("express");
const authorRouter = Router();
authorRouter.get("/", (req, res) => res.send("All authors"));
authorRouter.get("/:authorId", (req, res) => res.send(`Author: ${req.params.authorId}`));
module.exports = authorRouter;
// app.js
app.use("/authors", authorRouter); // all /authors/* requests go here
app.use("/books", bookRouter);
In your codebase:
backend/src/app/router.tsmounts all 32 collection routers — same pattern exactly
4. Controllers & Middleware
a. Middleware
- Any function that sits between the request coming in and the response going out
- Signature:
(req, res, next)— callnext()to pass to the next function in the chain - If you don't call
next()and don't send a response, the request just hangs
function logger(req, res, next) {
console.log(`${req.method} ${req.path}`);
next(); // pass control forward
}
app.use(logger); // runs on every request
Common middleware uses:
- Auth checks (is this user logged in?)
- Request validation (is the body shaped correctly?)
- Logging, CORS, JSON parsing
b. Controllers
- Controllers are just functions that handle a specific route — they're also middleware, but their job is to be the final handler that sends the response
- Named by convention:
getAuthorById,createStory,deleteEpisode
// controllers/authorController.js
async function getAuthorById(req, res) {
const { authorId } = req.params;
const author = await db.getAuthorById(Number(authorId));
if (!author) {
res.status(404).send("Author not found");
return; // must return — sending a response doesn't stop the function
}
res.send(`Author: ${author.name}`);
}
module.exports = { getAuthorById };
// routes/authorRouter.js — wire the controller to the route
const { getAuthorById } = require("../controllers/authorController");
authorRouter.get("/:authorId", getAuthorById);
In your codebase: every collection has a
{name}.controller.ts— these are exactly this pattern
c. Response Methods
| Method | Use |
|---|---|
res.json(data) | Send JSON — use this for APIs |
res.send(data) | General purpose, auto-detects type |
res.status(404).send(...) | Set status code + send — must chain |
res.redirect("/path") | Redirect client to another URL |
d. Error Handling
- Wrap async controllers in
try/catch, or just throw — Express v5 auto-catches - A special error middleware with 4 params
(err, req, res, next)catches all bubbled errors - Place it at the very end of
app.js
// global error handler — must have 4 params
app.use((err, req, res, next) => {
res.status(err.statusCode || 500).send(err.message);
});
- You can create custom error classes to carry a status code:
class NotFoundError extends Error {
constructor(message) {
super(message);
this.statusCode = 404;
}
}
// then in a controller:
throw new NotFoundError("Author not found"); // bubbles up to error handler
e. The next function
| Call | What happens |
|---|---|
next() | Pass to next middleware |
next(error) | Skip to error handler middleware |
f. Folder Structure
express-app/
├─ routes/ → routers (one per resource)
├─ controllers/ → handler functions
├─ errors/ → custom error classes
├─ app.js → server setup, mounts routers, error handler at bottom